Sports Results Completed#87
Conversation
chrisjamiecarter
left a comment
There was a problem hiding this comment.
Hey @DSills735 👋,
Excellent work on your Sports Results Notifier project submission 🎉!
I have performed a peer review. Review/ignore any comments as you wish.
Your code is clean, well-organised, and follows modern C# conventions like file-scoped namespaces. You've done a great job with defensive programming — null checks on HTML nodes, TryParse for score validation, and using environment variables for credentials are all solid practices. The project builds successfully with zero warnings, which is a great starting point.
Project-Wide Patterns:
- Static method coupling — All public methods are static, creating tight coupling between classes. Introducing interfaces and instance-based services would improve testability.
- Synchronous I/O — Web scraping and email sending are synchronous. Converting to async/await would make the application more responsive.
- No error handling for network calls — If the basketball website is unreachable, the app will crash. A try-catch around the web request would make it more resilient.
I will go ahead and mark as approved, keep up the excellent work on the next projects! 😊
Thanks,
@chrisjamiecarter 👍
| { | ||
| public static void Main(string[] args) | ||
| { | ||
| TimeOnly runningTime = new TimeOnly(23, 0, 0); // 11 PM |
There was a problem hiding this comment.
🟡 Magic Number
💡 The scheduled run time (23:00) is hardcoded as a magic number. Consider extracting it to a named constant or configuration for better clarity and maintainability:
private const int ScheduledHour = 23;
private static readonly TimeOnly RunningTime = new(ScheduledHour, 0, 0);| TimeOnly now = TimeOnly.FromDateTime(DateTime.Now); | ||
| DateOnly today = DateOnly.FromDateTime(DateTime.Now); |
There was a problem hiding this comment.
🟡 Redundant DateTime.Now Calls
💡 DateTime.Now is called twice within the same loop iteration. While the time difference is negligible here, it's cleaner and more consistent to capture it once and reuse:
DateTime now = DateTime.Now;
TimeOnly currentTime = TimeOnly.FromDateTime(now);
DateOnly today = DateOnly.FromDateTime(now);| lastRun = today; | ||
| } | ||
|
|
||
| Thread.Sleep(10000); |
There was a problem hiding this comment.
🟠 Blocking Sleep on Main Thread
💡 Thread.Sleep(10000) blocks the current thread and prevents clean application shutdown. Consider using await Task.Delay(10000) and making Main async:
public static async Task Main(string[] args)
{
// ...
await Task.Delay(10000);
}This also improves responsiveness and enables graceful cancellation with CancellationToken.
| TimeOnly runningTime = new TimeOnly(23, 0, 0); // 11 PM | ||
| DateOnly lastRun = DateOnly.MinValue; | ||
|
|
||
| while (true) |
There was a problem hiding this comment.
🟠 No Graceful Shutdown Handling
💡 The application runs an infinite while(true) loop with no means to exit cleanly. Consider adding a CancellationToken or handling Console.CancelKeyPress to allow a graceful stop when the user presses Ctrl+C:
CancellationTokenSource cts = new();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
// Use cts.Token in your loop and Task.Delay| WinnerScore = winner == team1 ? team1Score : team2Score; | ||
| LoserScore = loser == team1 ? team1Score : team2Score; |
There was a problem hiding this comment.
🟡 Redundant Constructor Logic
💡 WinnerScore and LoserScore are computed from other properties passed to the constructor. They could be refactored as computed (expression-bodied) properties, eliminating the need to store them and simplifying the constructor:
public int WinnerScore => Team1Score > Team2Score ? Team1Score : Team2Score;
public int LoserScore => Team1Score < Team2Score ? Team1Score : Team2Score;This removes the ternary logic and Winner/Loser string comparisons from the constructor.
| public static void Scan() | ||
| { | ||
| HtmlWeb web = new HtmlWeb(); | ||
| HtmlDocument document = web.Load("https://www.basketball-reference.com/boxscores/"); |
There was a problem hiding this comment.
🟡 Synchronous Web Scraping
💡 HtmlWeb.Load() performs synchronous I/O, blocking the thread. Consider using the async overload await web.LoadAsync(...) for better resource utilization, and make Scan() an async method.
| gameList.Add(new Game(team1, team1Score, team2, team2Score, winner, loser)); | ||
| } | ||
|
|
||
| Email.Send(gameList); |
There was a problem hiding this comment.
🟠 Tight Coupling via Static Call
💡 Email.Send(gameList) is called directly as a static method, creating tight coupling between BasketballScanner and Email. Consider making these instance methods and using dependency injection or passing an abstraction (e.g., IEmailService) to improve testability and flexibility.
| var team2Node = game.SelectSingleNode(".//tr[2]/td[1]/a"); | ||
| var team2ScoreNode = game.SelectSingleNode(".//tr[2]/td[2]"); | ||
|
|
||
| if (team1Node == null || team1ScoreNode == null || team2Node == null || team2ScoreNode == null) |
| if (!int.TryParse(team1ScoreNode.InnerText.Trim(), out int team1Score) || | ||
| !int.TryParse(team2ScoreNode.InnerText.Trim(), out int team2Score)) |
There was a problem hiding this comment.
🟢 Input Validation with TryParse
| string fromEmail = Environment.GetEnvironmentVariable("EMAIL_FROM") | ||
| ?? throw new InvalidOperationException("EMAIL_FROM environment variable is not set."); | ||
| string toEmail = Environment.GetEnvironmentVariable("EMAIL_TO") | ||
| ?? throw new InvalidOperationException("EMAIL_TO environment variable is not set."); | ||
| string password = Environment.GetEnvironmentVariable("EMAIL_PASSWORD") | ||
| ?? throw new InvalidOperationException("EMAIL_PASSWORD environment variable is not set."); |
No description provided.